12984 2020-02-27 2024-10-30

本篇重点在于分析Spring MVC与Servlet标准的整合,下节将详细讨论Spring MVC的启动/加载流程、处理请求的具体流程。

一、介绍

Spring框架提供了构建Web应用程序的全功能MVC模块。通过策略接口 ,Spring框架是高度可配置的,而且支持多种视图技术。Spring MVC框架并不知道使用的视图,所以不会强迫你只使用JSP或特定某一种技术。Spring MVC分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。

Spring的MVC是基于Servlet功能实现的,通过实现Servlet接口的DispatcherServlet来封装其核心功能的实现,通过将请求分派给处理程序,同时带有可配置的处理程序映射、视图解析、本地语言、主题解析以及上传文件支持。默认的处理程序是非常简单的Controller接口,只有一个方法ModelAndView handleRequest(request, response)。

Spring MVC或者其他比较成熟的MVC框架而言,解决的问题无外乎一下几点。

  1. 将Web页面的请求传给服务器。
  2. 根据不同的请求处理不同的逻辑单元。
  3. 返回处理结果数据并跳转至响应页面。

这里假设读者具有一定的使用经验,下文将直接从代码出发。

二、ContextLoaderListener

web.xml对于Spring MVC项目是必不可少的,下面是一个简单的例子

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>/index.do</welcome-file>
    </welcome-file-list>
</web-app>

当使用编程方式的时候我们可以直接将Spring配置信息作为参数传入Spring容器中,例如

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

但是在Web下,我们需要更多的是与Web环境相互结合,通常的办法是将路径以context-param的方式注册并使用ContextLoaderListener进行监听读取。

ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法。

使用 ServletContextListener 接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加任意的对象。这个对象在ServletContext启动的时候被初始化,然后在ServletContext整个运行期间都是可见的。

每一个Web应用都有一个ServletContext与之相关联。ServletContext对象在应用启动时被创建,在应用关闭的时候被销毁。ServletContext在全局范围内有效,类似于应用中的一个全局变量。

在ServletContextListener中的核心逻辑便是初始化 WebApplicationContext 实例并存放至ServletContext中。

1、ServletContextListener的使用

正式分析代码前我们同样还是首先了解ServletContextListener的使用。

  1. 创建自定义的ServletContextListener

首先我们创建ServletContextListener,目标是在系统启动时添加自定义的属性,以便于在全局范围内可以随时使用。系统启动的时候会调用ServletContextListener实现类的contextInitialized方法,所以需要在这个方法中实现我们的初始化逻辑。

package javax.servlet;

public interface ServletContextListener extends EventListener {
    
    public void contextInitialized ( ServletContextEvent sce );

    public void contextDestroyed ( ServletContextEvent sce );
}

public class MyDataContextListener implements ServletContextListener {  
    private ServletContext context = null;
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        this.context = sce.getServletContext();
        // 添加自定义属性
        context.setAttribute("myData", "this is myData");
        
    }
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        this.context = null;
    }
}
  1. 注册监听器

在web.xml文件中需要注册自定义的监听器。

<listener>
    <listener-class>
        site.xiaokui.test.MyDataContextListener
    </listener-class>
</listener>
  1. 测试

一旦Web应用启动的时候,我们就能在任意的Servlet或者JSP中通过下面的方法获取我们初始化的参数,如下

String myData = (String) getServletContext().getAttribute("myData");

2、ContextLoaderListener**

分析了ServletContextListener的使用方式后再来分析Spring中的ContextLoaderListener的实现就容易理解的多,虽然ContextLoaderListener实现的逻辑要复杂的多,但是大致的套路还是万变不离其宗。

ServletContext启动之后会调用ServletContextListener的contextInitialized方法,那么我们就从这个方法开始分析。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

	private ContextLoader contextLoader;

	public ContextLoaderListener() {
	}

	public ContextLoaderListener(WebApplicationContext context) {
		super(context);
	}

	@Override
	public void contextInitialized(ServletContextEvent event) {
        // 进入这一行
		initWebApplicationContext(event.getServletContext());
	}

	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}
}

这里涉及到一个常用类WebApplicationContext:在Web应用中,我们会用到WebApplicationContext,WebApplicationContext继承自ApplicationContext,在ApplicationContext的基础上又追加了一些特定于Web的操作及属性,非常类似与我们通过编程方式使用Spring时的ClassPathXmlApplication类提供的功能。继续跟踪代码

// 来自类 ContextLoaderListener extends ContextLoader
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
        // web.xml中存在多次ContextLoader定义
        throw new IllegalStateException(
                "Cannot initialize context because there is already a root application context present - " +
                "check whether you have multiple ContextLoader* definitions in your web.xml!");
    }

    Log logger = LogFactory.getLog(ContextLoader.class);
    servletContext.log("Initializing Spring root WebApplicationContext");
    if (logger.isInfoEnabled()) {
        logger.info("Root WebApplicationContext: initialization started");
    }
    long startTime = System.currentTimeMillis();

    try {
        // Store context in local instance variable, to guarantee that
        // it is available on ServletContext shutdown.
        if (this.context == null) {
            // 初始化Context
            this.context = createWebApplicationContext(servletContext);
        }
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent ->
                    // determine parent for root web application context, if any.
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                // 加载xml配置,并刷新WebApplicationContext
                // 这一步很关键
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = this.context;
        }
        else if (ccl != null) {
            currentContextPerThread.put(ccl, this.context);
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
        }
        if (logger.isInfoEnabled()) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
        }

        return this.context;
    }
    catch (RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
        throw ex;
    }
    catch (Error err) {
        logger.error("Context initialization failed", err);
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
        throw err;
    }
}

initWebApplicationContext方法主要是体现了创建WebApplicationContext实例的一个功能架构,从方法中我们看到了初始化的大致步骤。

1、initWebApplicationContext

这个标题没什么特别的含义,只是为了区分DispatchServlet中的initWebApplicationContext。

2、WebApplicationContext存在性的验证

在配置中只允许声明一次ServletContextListener,多次声明会扰乱Spring的执行逻辑,所以这里首先要做的就是对此验证,在Spring中如果创建WebApplicationContext实例会记录在ServletContext中以方便全局调用,而使用的key就是WebApplication.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以验证的方式就是查看ServletContext实例中是否存在对应key的属性

3、createWebApplicationContext

如果通过验证,则Spring将创建WebApplicationContext实例的工作委托给了createWebApplication方法。

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    Class<?> contextClass = determineContextClass(sc);
    // 父类.class.isAssignableFrom(子类)时返回true,否则返回false
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

// public static final String CONTEXT_CLASS_PARAM = "contextClass";
protected Class<?> determineContextClass(ServletContext servletContext) {
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
    // 兼容自定义 WebApplicationContext 实现类
    if (contextClassName != null) {
        try {
            return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
        }
        catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load custom context class [" + contextClassName + "]", ex);
        }
    }
    else {
        // 走默认策略,默认就是 XmlWebApplicationContext
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        try {
            return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
        }
        catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load default context class [" + contextClassName + "]", ex);
        }
    }
}

其中,在 ContextLoader 类中有这样的静态代码块

static {
    // Load default strategy implementations from properties file.
    // This is currently strictly internal and not meant to be customized
    // by application developers.
    try {
        ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
        throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
    }
}

根据以上静态代码块的内容,我们推断在当前类ContextLoader同样目录下必定会存在属性文件 ContextLoader.properties,查看后果然存在,内容如下

# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

综合以上代码分析,在初始化的过程中,程序首先会读取ContextLoader类的同目录下的属性文件ContextLoader.properties,并根据其中的配置提取将要实现WebApplicationApplicationContext接口的实现类,并根据这个实现类通过反射方式进行实例的创建。

3、加载xml配置,并刷新WebApplicationContext,然后实例记录在servletContext中。

4、映射当前的类加载器与创建的WebApplicationContext实例到全局变量currentContextPerThread中。

三、configureAndRefreshWebApplicationContext

跟踪代码,如下所示

// 来源于类 ContextLoader
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
        if (idParam != null) {
            wac.setId(idParam);
        }
        else {
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(sc.getContextPath()));
        }
    }
	// 互相塞进去
    wac.setServletContext(sc);
    // 我们重点关注下这一句,这里值为contextConfigLocation
    String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
    if (configLocationParam != null) {
        // 这一步中已经初始化了 StandardServletEnvironment
        wac.setConfigLocation(configLocationParam);
    }

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    // 获取系统环境,默认为 StandardServletEnvironment
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        // 将WebApplicationContex中的属性配置到 StandardServletEnvironment
        // 主要是servletContextInitParams、servletConfigInitParams两个属性分别保存着对象ServletContext servletContext, ServletConfig servletConfig,后者这里为null
        ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
    }

    customizeContext(sc, wac);
    // XmlWebApplicationContext
    wac.refresh();
}

1、setConfigLocation

// 来源于类 AbstractRefreshableConfigApplicationContext
public void setConfigLocation(String location) {
    setConfigLocations(StringUtils.tokenizeToStringArray(location, CONFIG_LOCATION_DELIMITERS));
}

public void setConfigLocations(@Nullable String... locations) {
    if (locations != null) {
        Assert.noNullElements(locations, "Config locations must not be null");
        this.configLocations = new String[locations.length];
        for (int i = 0; i < locations.length; i++) {
            // 解析${}占位符,如果有的话
            this.configLocations[i] = resolvePath(locations[i]).trim();
        }
    }
    else {
        this.configLocations = null;
    }
}

protected String resolvePath(String path) {
    return getEnvironment().resolveRequiredPlaceholders(path);
}

@Override
public ConfigurableEnvironment getEnvironment() {
    // 第一次进来为null,后面就不为null,默认实现为StandardServletEnvironment
    if (this.environment == null) {
        this.environment = createEnvironment();
    }
    return this.environment;
}

@Override
protected ConfigurableEnvironment createEnvironment() {
    return new StandardServletEnvironment();
}

2、getEnvironment

// 来自类 AbstractApplicationContext
@Override
public ConfigurableEnvironment getEnvironment() {
    // 第一次进来为null,后面就不为null,默认实现为StandardServletEnvironment
    if (this.environment == null) {
        this.environment = createEnvironment();
    }
    return this.environment;
}

protected ConfigurableEnvironment createEnvironment() {
    return new StandardEnvironment();
}

public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {

	/** Servlet context init parameters property source name: {@value}. */
	public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";

	/** Servlet config init parameters property source name: {@value}. */
	public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";

	/** JNDI property source name: {@value}. */
	public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";

	@Override
	protected void customizePropertySources(MutablePropertySources propertySources) {
		propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
		propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
		if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
			propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
		}
		super.customizePropertySources(propertySources);
	}

	@Override
	public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
		WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
	}

}

// 替换占位符,保证加载配置的优先级
public static void initServletPropertySources(MutablePropertySources sources,
        @Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {

    Assert.notNull(sources, "'propertySources' must not be null");
    String name = StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME;
    if (servletContext != null && sources.contains(name) && sources.get(name) instanceof StubPropertySource) {
        sources.replace(name, new ServletContextPropertySource(name, servletContext));
    }
    name = StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME;
    if (servletConfig != null && sources.contains(name) && sources.get(name) instanceof StubPropertySource) {
        sources.replace(name, new ServletConfigPropertySource(name, servletConfig));
    }
}

其中在类型为MutablePropertySources的propertySources中,大致存储着以下5个PropertySource,优先级依次降低,如下

1、StubPropertySource(servletConfigInitParams)

这是一个只有名称标识,没有任何值的属性源,只用作占位符,当实际的属性源对象不能在ApplicationContext应用上下文创建的时候被立即初始化,则会使用它来占位,以保证属性源集合的搜索顺序。

举个例子:在Web环境中,创建StandardServletEnvironment对象时,会先调用customPropertySources追加ServletConfig和ServletContext两个属性源对象,但是此时并没有这两个对象的引用(两个对象的引用通过initPropertySources初始化时传入),因此会创建StubPropertySource对象占位,当初始化时再用实际属性源替换掉(根据name匹配)占位对象。

StubPropertySource@795553795 {name='servletConfigInitParams', properties=java.lang.Object@12ed85a3}

2、StubPropertySource(servletContextInitParams)

大致同上,区别是一个对Servlet、一个对Context。

StubPropertySource@476472887 {name='servletContextInitParams', properties=java.lang.Object@dae2226}

3、JndiPropertySource(jndiProperties)

JNDI:Java Naming and Directory Interface Java命名和目录接口,比较典型的例子就是Class.forName("com.mysql.jdbc.Driver")

JndiPropertySource@207182756 {name='jndiProperties', properties=org.springframework.jndi.JndiLocatorDelegate@2d062814}

4、PropertiesPropertySource(systemProperties)

存放所有系统属性,形如

java.vm.name=OpenJDK 64-Bit Server VM, ignore.endorsed.dirs=, file.encoding=UTF-8, com.sun.management.jmxremote.password.file=/home/hk/.IntelliJIdea2019.2/system/tomcat/Tomcat_8_5_471_spring-web-demo/jmxremote.password, java.specification.version=1.8, intellij.debug.agent=true

5、PropertiesPropertySource(systemEnvironment)

存放所有环境变量,形如

PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/sbin:/usr/sbin, XAUTHORITY=/home/hk/.Xauthority, XMODIFIERS=@im=fcitx, GDMSESSION=deepin, USER=hk, CLASSPATH=/usr/dev/apache-tomcat-8.5.47/bin/bootstrap.jar:/usr/dev/apache-tomcat-8.5.47/bin/tomcat-juli.jarHOME=/home/hk, SHLVL=0

3、customizeContext

当我们设置了CONTEXT_INITIALIZER_CLASSES_PARAM参数指定初始化WebApplicationContext的类,这一步会保证我们自定义的Context会执行相应初始化方法,否则这一步为空逻辑实现。

// public static final String CONTEXT_CLASS_PARAM = "contextClass";
protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
    List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
            determineContextInitializerClasses(sc);
    for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
        Class<?> initializerContextClass =
                GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
        if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
            throw new ApplicationContextException(String.format(
                    "Could not apply context initializer [%s] since its generic parameter [%s] " +
                    "is not assignable from the type of application context used by this " +
                    "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
                    wac.getClass().getName()));
        }
        this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
    }

    AnnotationAwareOrderComparator.sort(this.contextInitializers);
    for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
        initializer.initialize(wac);
    }
}

4、wac.refresh**

继续跟踪,如下

// XmlWebApplicationContext
wac.refresh();

// 来自父类 AbstractApplicationContext
@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // Prepare this context for refreshing.
        prepareRefresh();

        // Tell the subclass to refresh the internal bean factory.
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);

        try {
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);

            // Invoke factory processors registered as beans in the context.
            invokeBeanFactoryPostProcessors(beanFactory);

            // Register bean processors that intercept bean creation.
            registerBeanPostProcessors(beanFactory);

            // Initialize message source for this context.
            initMessageSource();

            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();

            // Initialize other special beans in specific context subclasses.
            onRefresh();

            // Check for listener beans and register them.
            registerListeners();

            // Instantiate all remaining (non-lazy-init) singletons.
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            finishRefresh();
        }

        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                        "cancelling refresh attempt: " + ex);
            }

            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();

            // Reset 'active' flag.
            cancelRefresh(ex);

            // Propagate exception to caller.
            throw ex;
        }

        finally {
            // Reset common introspection caches in Spring's core, since we
            // might not ever need metadata for singleton beans anymore...
            resetCommonCaches();
        }
    }
}

熟悉的代码,请跳转到上一篇继续阅读,谢谢。

.................................................................................

假设读者已经看完了上一篇,那么此时下面代码已经执行完成。

/**
 * Initialize the root web application context.
 */
@Override
public void contextInitialized(ServletContextEvent event) {
    // 进入这一行
    initWebApplicationContext(event.getServletContext());
}

此时Tomcat已经得到了一个完整的ServletContext,接下来我们把断点打在DispatcherServlet的构造方法上,再经历过27次F8后,逻辑终于从org.apache.catalina包回到了熟悉的Spring源码中。

特别的,此时有以下日志输出

20:25:07.002 [0.1][INFO] web.context.ContextLoader 271: Root WebApplicationContext: initialization started
# 中间省略几百行
20:25:22.030 [0.1][INFO] web.context.ContextLoader 307: Root WebApplicationContext initialized in 15021 ms

注意,DispatcherServlet的构造方法并不是Servlet的入口,构造方法中没有包含任何逻辑实现,真正的逻辑实现是交给Servlet标准中的init方法。

四、初始化DispatcherServlet

在Spring中,ContextLoaderListener只是辅助功能,用于创建WebApplicationContext类型实例,而真正的逻辑实现其实是在DispatchServlet中进行的,DispatchServlet是实现Servlet接口的实现类。

1、Servlet概述

Servlet是一个Java编写的程序,基于HTTP协议,在服务器端运行,主要处理客户端的请求并将处理结果发送到客户端。Servlet的生命周期是由Servlet容器来控制的,分为3个阶段:初始化、运行和销毁。

1、初始化阶段

  1. Servlet容器加载Servlet类,把Servlet类.class文件的数据读到内存中。

  2. Servlet容器创建一个ServletConfig对象,ServletConfig对象包含了Servlet的初始化配置信息。

  3. Servlet容器创建一个Servlet对象。

  4. Servlet容器调用Servlet对象的init方法进行初始化。

2、运行阶段

当Servlet容器收到一个请求时,Servlet容器会针对这个请求创建ServletRequest和ServletResponse对象,然后调用service方法,并把这两个参数传递个service方法,service方法通过ServletRequest对象获得请求的信息,并处理请求。再通过ServletResponse对象生成这个请求的响应结果,最后销毁ServletRequest和ServletResponse对象。我们不管这个请求是post提交的还是get提交,最终这个请求都会由service方法来处理。

// 来自javax.servlet.http.HttpServlet
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
// 请求方法名都是大写的 GET
if (method.equals(METHOD_GET)) {
    long lastModified = getLastModified(req);
    if (lastModified == -1) {
    // servlet doesn't support if-modified-since, no reason
    // to go through further expensive logic
    doGet(req, resp);
    } else {
    long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
    if (ifModifiedSince < (lastModified / 1000 * 1000)) {
        // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
        maybeSetLastModified(resp, lastModified);
        doGet(req, resp);
    } else {
        resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
    }
} else if (method.equals(METHOD_HEAD)) {
    long lastModified = getLastModified(req);
    maybeSetLastModified(resp, lastModified);
    doHead(req, resp);

} else if (method.equals(METHOD_POST)) {
    doPost(req, resp);

} else if (method.equals(METHOD_PUT)) {
    doPut(req, resp);	

} 
// 省略其他

3、销毁阶段

当Web应用被终止时,Servlet容器会先调用Servlet对象的destroy方法,然后再销毁Servlet对象,同时也会销毁与Servlet对象相关联的ServletConfig对象。我们可以在destroy方法的实现中,释放Servlet所占用的资源,如关闭数据库连接,关闭文件输入输出流等。

Servlet框架是由两个Java包组成,分别为javax.servlet和java.servlet.http。在java.servlet包中定义了所有Servlet类都必须实现或扩展的通用接口和类,在javax.servlet.http包中定义了采用HTTP通信协议的HttpServlet类。

Servlet被设计成请求驱动,Servlet的请求可能包含多个数据项,当Web容器接受到某个Servlet请求时,Servlet把请求封装成一个HttpServletRequest对象,然后把对象传给Servlet的对应的服务方法。

Http的请求方式包括delete、get、options、post、put、trace,在HttpServlet类中分别提供了相应的服务方法,它们分别是doDelete、doGet、doOptions、doPost、doTrace方法。

2、静态代码块

在类DispatcherServlet中有这么一段静态代码(文件名为DispatcherServlet.properties,注意区分之前的 ContextLoader.properties),如下

// 在初始化相应的父类属性之后
static {
    // Spring Web内置,不能由用户自定义
    // Load default strategy implementations from properties file.
    // This is currently strictly internal and not meant to be customized
    // by application developers.
    try {
        // DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
        ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
        throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
    }
}

加载properties文件内容如下

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
# 国际化适配
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
# 主题适配
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
# HandlerMapping,处理url映射
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
	org.springframework.web.servlet.function.support.RouterFunctionMapping
# Handler适配器
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
	org.springframework.web.servlet.function.support.HandlerFunctionAdapter

# 异常处理
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
# 视图翻译
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
# 视图解析
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
# 适配Flash
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

这段代码有什么用呢?主要是提供Spring MVC中一些默认组件,当用户没有自定义组件时,Spring就会使用这些默认组件(下篇将会详细展开讨论)。

3、init方法**

这里有两个概念需要着重区分,一个是ServletContext的入口,一个是Servlet的入口。ServletContextListener是ServletContext的入口,而init方法就是Servlet的入口。

在Servlet初始化阶段会调用其init方法,所以我们首先要查看在DispatcherServlet中是否重写了init方法,我们在其父类HttpServletBean中找到了该方法。

// DispatcherServlet 继承 FrameworkServlet 继承 HttpServletBean
// 来自于类 HttpServletBean
@Override
public final void init() throws ServletException {
    // Set bean properties from init parameters.
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    // 一般情况下空,所以可以直接跳过这个if
    if (!pvs.isEmpty()) {
        try {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
            initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            if (logger.isErrorEnabled()) {
                logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }

    // Let subclasses do whatever initialization they like.
    initServletBean();
}

DispatcherServlet的初始化过程主要是通过将当前的Servlet类型实例转换为BeanWrapper类型实例,以便使用Spring中提供的注入功能进行对应属性的注入。这些属性如contextAttribute、contextClass、nameSpace、contextConfigLacation等,都可以在web.xml文件中以初始化参数的方式配置在Servlet的声明中。

DispatcherServlet继承自FrameworkServlet,FrameworkServlet类上包含对应的同名属性,Spring会保证这些参数被注入到对应的值上。属性注入主要包含一下几个步骤。

1、封装及验证初始化参数

ServletConfigPropertyValues除了封装属性外还有对属性验证的功能。

private static class ServletConfigPropertyValues extends MutablePropertyValues {

    public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties)
        throws ServletException {

        Set<String> missingProps = (requiredProperties != null && !requiredProperties.isEmpty()) ?
                new HashSet<String>(requiredProperties) : null;

        Enumeration en = config.getInitParameterNames();
        while (en.hasMoreElements()) {
            String property = (String) en.nextElement();
            Object value = config.getInitParameter(property);
            addPropertyValue(new PropertyValue(property, value));
            if (missingProps != null) {
                missingProps.remove(property);
            }
        }

        // Fail if we are still missing properties.
        if (missingProps != null && missingProps.size() > 0) {
            throw new ServletException(
                "Initialization from ServletConfig for servlet '" + config.getServletName() +
                "' failed; the following required properties were missing: " +
                StringUtils.collectionToDelimitedString(missingProps, ", "));
        }
    }
}

从代码中得知,封装属性主要是对初始化的参数进行封装,也就是Servlet中配置的<init-param>中配置的封装。当然,用户可以通过对requiredProperties参数的初始化来强制验证某些属性的必要性,这样,在属性封装的过程中,一旦检测到requiredProperties中的属性没有指定初始值,就会抛出异常。

2、将当前Servlet实例转换为BeanWrapper实例

PropertyAccessorFactory.forBeanPropertyAccess是Spring提供的工具类方法,主要是将指定实例转换为Spring中可以处理的BeanWrapper类型的实例。

public abstract class PropertyAccessorFactory {

	public static BeanWrapper forBeanPropertyAccess(Object target) {
		return new BeanWrapperImpl(target);
	}

	public static ConfigurablePropertyAccessor forDirectFieldAccess(Object target) {
		return new DirectFieldAccessor(target);
	}
}

3、注册相对于Resource的属性编辑器

属性编辑器,我们在上文已经介绍并且分析过其原理,这里使用属性编辑器的目的就是在对当前实例(DispatcherServlet)属性注入过程中一旦遇到Resource类型的属性就会使用ResourceEditor去解析。

4、属性注入

BeanWrapper为Spring中的方法,支持Spring的自动注入。其实我们最常用的属性注入无非是contextAttribute、contextClass、nameSpace、contextConfigLocation等属性。

4、initServletBean**

在ContextLoaderListener加载的时候已经创建了WebApplication实例,而在这个函数中最重要的就是对这个实例进行进一步的补充初始化。

继续查看iniServletBean方法,父类FrameworkServlet覆盖了HttpServletBean中的iniServletBean方法,如下

// 来源于类 FrameworkServlet
/**
 * Overridden method of {@link HttpServletBean}, invoked after any bean properties
 * have been set. Creates this servlet's WebApplicationContext.
 */
@Override
protected final void initServletBean() throws ServletException {
    getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
    if (this.logger.isInfoEnabled()) {
        this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
    }
    long startTime = System.currentTimeMillis();

    try {
        // 注意这一步,有点熟悉啊
        this.webApplicationContext = initWebApplicationContext();
        // 留给子类覆盖,这是空实现
        initFrameworkServlet();
    }
    catch (ServletException ex) {
        this.logger.error("Context initialization failed", ex);
        throw ex;
    }
    catch (RuntimeException ex) {
        this.logger.error("Context initialization failed", ex);
        throw ex;
    }

    if (this.logger.isInfoEnabled()) {
        long elapsedTime = System.currentTimeMillis() - startTime;
        this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
                elapsedTime + " ms");
    }
}

上面的方法设计了计时器来统计初始化的执行时间,而且提供了一个扩展方法initFrameworkServlet用于子类的覆盖操作,而作为关键的初始化逻辑实现则委托给了initWebApplicationContext方法。

五、initWebApplicationContext

为了避免混淆,这里列下上文提到的initWebApplicationContext方法,如下

// 来源于类 FrameworkServlet
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    // 列几句关键代码
    if (this.context == null) {
    this.context = createWebApplicationContext(servletContext);
	}
    if (this.context instanceof ConfigurableWebApplicationContext) {
        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
        if (!cwac.isActive()) {
            // The context has not yet been refreshed -> provide services such as
            // setting the parent context, setting the application context id, etc
            if (cwac.getParent() == null) {
                // The context instance was injected without an explicit parent ->
                // determine parent for root web application context, if any.
                ApplicationContext parent = loadParentContext(servletContext);
                cwac.setParent(parent);
            }
            configureAndRefreshWebApplicationContext(cwac, servletContext);
        }
    }
}

回到正题,initWebApplicationContext方法的主要工作就是创建或刷新WebApplicationContext实例并对Servlet功能所使用的变量进行初始化。

// 来源于类 FrameworkServlet
// 注意区分类ContextLoaderListener中initWebApplicationContext(event.getServletContext())
protected WebApplicationContext initWebApplicationContext() {
    // 根rootContext,就是之前创建的WebApplicationContext,需要注意的是如果上一步发生了异常/错误,那么这里或抛出对应的异常/错误
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
	// 第一个判断,为null
    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -> use it
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                // 刷新上下文环境
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    // 第二个初始化判断,为null
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    // 第三个,自己创建自己的WebApplicationContext,注意这个WebApplicationContext是rootContext的子Context
    if (wac == null) {
        // No context instance is defined for this servlet -> create a local one
        wac = createWebApplicationContext(rootContext);
    }
	// 子WebApplicationContext初始化完成,刷新自己
    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        
        // 重点关注一下这一行,后文将单独拿个章节
        onRefresh(wac);
    }
	// 注册context
    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                    "' as ServletContext attribute with name [" + attrName + "]");
        }
    }
    return wac;
}

1、两个WebApplicationContex?

首先要注意这里的两个WebApplicationContex(rootContext和wac)

简而言之:

  • rootContext是全局Context,wac是它的子Context,它们担任的角色不一样。
  • rootContext定义的是Spring中所需要的bean或配置,wac定义的是Servlet(MVC环境)中所需要的bean或配置。
  • rootContext不能引用wac中的bean或配置,但wac却能引用rootContext中的bean或配置。
  • 大部分情况下rootContext是没有什么用的,因为在Spring中一般只有一个Servlet。

下面还是继续跟踪,对于本方法中的初始化主要包含以下几个部分(熟悉的场景?需要注意的是,虽然方法名和大体逻辑是一样的,但代码上还是有些小小上的区别

2、createWebApplicationContext

一般都是直接创建,但不排除之前已经存在的可能,WebApplicationContext的寻找及创建包括以下几个步骤。

1、通过构造方法的注入进行初始化

当进入initWebApplicationContext方法后通过判断this.webApplicationContext != null后,便可以确定是否通过构造方法来初始化的,没有进行过初始化则进行下一步。

2、通过contextAttribute进行初始化

通过在web.xml文件中配置的Servlet参数contextAttribute来查找ServletContext中对应的属性,默认为WebApplicationContext.class.getName() + “.ROOT”,也就是在ContextLoaderListener加载时会创建WebApplicationContext实例,并将实例以WebApplicationContext.class.getName() + “.ROOT”为key放入ServletContext中,当然读者可以重写初始化逻辑使用自己创建的WebApplicationContext,并在Servlet的配置中通过初始化参数contextAttribute指定key。

protected WebApplicationContext findWebApplicationContext() {
    String attrName = getContextAttribute();
    if (attrName == null) {
        return null;
    }
    WebApplicationContext wac =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    if (wac == null) {
        throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
    }
    return wac;
}

3、创建WebApplicationContext实例

如果通过以上两种方式并没有找到任何突破,那就没有办法了,只能在这里重新创建新的实例了。

protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
    return createWebApplicationContext((ApplicationContext) parent);
}

protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
    // 获取Servlet的初始化参数contextClass,如果没有配置默认为 XMLWebApplicationContext.class
    Class<?> contextClass = getContextClass();
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Servlet with name '" + getServletName() +
                "' will try to create custom WebApplicationContext context of class '" +
                contextClass.getName() + "'" + ", using parent context [" + parent + "]");
    }
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
                "Fatal initialization error in servlet with name '" + getServletName() +
                "': custom WebApplicationContext class [" + contextClass.getName() +
                "] is not of type ConfigurableWebApplicationContext");
    }
    // 通过反射方式实例化contextClass
    ConfigurableWebApplicationContext wac =
            (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	// 这里又是new StandardServletEnvironment()
    wac.setEnvironment(getEnvironment());
    // 会合并父WebApplicationContext Environment,这里有对profile的处理
    wac.setParent(parent);
    // 获取contextConfigLocation属性,配置在Servlet初始化参数中,一般为null
    wac.setConfigLocation(getContextConfigLocation());
	// 初始化Spring环境
    configureAndRefreshWebApplicationContext(wac);
    return wac;
}

1、setParent

// 来自于类 AbstractApplicationContext
@Override
public void setParent(@Nullable ApplicationContext parent) {
    this.parent = parent;
    if (parent != null) {
        // 父Context内容其实也是 new StandardServletEnvironment
        Environment parentEnvironment = parent.getEnvironment();
        if (parentEnvironment instanceof ConfigurableEnvironment) {
            // 关键在于这一步,里面有对profile的处理
            getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
        }
    }
}

// 来自于类 AbstractEnvironment
@Override
public void merge(ConfigurableEnvironment parent) {
    // 两者一样,可跳过
    for (PropertySource<?> ps : parent.getPropertySources()) {
        if (!this.propertySources.contains(ps.getName())) {
            this.propertySources.addLast(ps);
        }
    }
    // 寻找激活的profile,这一步是我们关注的
    // Spring会遍历多个环境遍历资源以寻找激活的profile
    String[] parentActiveProfiles = parent.getActiveProfiles();
    if (!ObjectUtils.isEmpty(parentActiveProfiles)) {
        synchronized (this.activeProfiles) {
            Collections.addAll(this.activeProfiles, parentActiveProfiles);
        }
    }
    // 寻找默认名为default的profile
    String[] parentDefaultProfiles = parent.getDefaultProfiles();
    if (!ObjectUtils.isEmpty(parentDefaultProfiles)) {
        synchronized (this.defaultProfiles) {
            this.defaultProfiles.remove(RESERVED_DEFAULT_PROFILE_NAME);
            Collections.addAll(this.defaultProfiles, parentDefaultProfiles);
        }
    }
}

@Override
public String[] getActiveProfiles() {
    return StringUtils.toStringArray(doGetActiveProfiles());
}

protected Set<String> doGetActiveProfiles() {
    synchronized (this.activeProfiles) {
        if (this.activeProfiles.isEmpty()) {
            // spring.profiles.active
            String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
            if (StringUtils.hasText(profiles)) {
                setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
                        StringUtils.trimAllWhitespace(profiles)));
            }
        }
        return this.activeProfiles;
    }
}

public String getProperty(String key) {
    return getProperty(key, String.class, true);
}

@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
    if (this.propertySources != null) {
        // 遍历PropertySource
        for (PropertySource<?> propertySource : this.propertySources) {
            if (logger.isTraceEnabled()) {
                logger.trace("Searching for key '" + key + "' in PropertySource '" +
                        propertySource.getName() + "'");
            }
            Object value = propertySource.getProperty(key);
            if (value != null) {
                if (resolveNestedPlaceholders && value instanceof String) {
                    value = resolveNestedPlaceholders((String) value);
                }
                logKeyFound(key, propertySource, value);
                return convertValueIfNecessary(value, targetValueType);
            }
        }
    }
    if (logger.isTraceEnabled()) {
        logger.trace("Could not find key '" + key + "' in any property source");
    }
    return null;
}

public String[] getDefaultProfiles() {
    return StringUtils.toStringArray(doGetDefaultProfiles());
}

// 默认内置了default profile
protected Set<String> doGetDefaultProfiles() {
    synchronized (this.defaultProfiles) {
        if (this.defaultProfiles.equals(getReservedDefaultProfiles())) {
            String profiles = getProperty(DEFAULT_PROFILES_PROPERTY_NAME);
            if (StringUtils.hasText(profiles)) {
                setDefaultProfiles(StringUtils.commaDelimitedListToStringArray(
                        StringUtils.trimAllWhitespace(profiles)));
            }
        }
        return this.defaultProfiles;
    }
}

2、configureAndRefreshWebApplicationContext**

无论是通过构造方法注入还是单独创建,都免不了会调用configureAndRefreshWebApplicationContext方法来对已经创建WebApplicationContext实例进行配置及刷新,那么这个步骤又做了那些工作呢?

// 来自于类 FrameworkServlet
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        else {
            // Generate default id...
            ServletContext sc = getServletContext();
            if (sc.getMajorVersion() == 2 && sc.getMinorVersion() < 5) {
                // Servlet <= 2.4: resort to name specified in web.xml, if any.
                String servletContextName = sc.getServletContextName();
                if (servletContextName != null) {
                    wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + servletContextName +
                            "." + getServletName());
                }
                else {
                    wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + getServletName());
                }
            }
            else {
                // Servlet 2.5's getContextPath available!
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                        ObjectUtils.getDisplayString(sc.getContextPath()) + "/" + getServletName());
            }
        }
    }
    // 又是互相塞进去
    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    // 这一行代码跟之前的有点不一样,值为dispatcher-servlet
    wac.setNamespace(getNamespace());
    // 注意这一行,后面会来回来的
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }
    // 空逻辑,留给子类实现
    postProcessWebApplicationContext(wac);
    // 如果使用了ApplicationContextInitializer自定义逻辑,那么在这里执行
    // 如果使用的不是boot的话,你别说,想要加进去自己的逻辑还有点稍稍有点啰嗦,下面给个例子
    applyInitializers(wac);
    // 熟悉的方法
    wac.refresh();
}

例子如下

public class TestServlet extends DispatcherServlet {
    
    public TestServlet() {
        super();
        this.setContextInitializers(new TestApplicationContextInitializer());
    }

    private static class TestApplicationContextInitializer implements ApplicationContextInitializer {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            System.out.println(applicationContext + "还没执行refresh之前调用了我");
        }
    }
}

无论调用方式如何变化,只要是使用ApplicationContext所提供的功能最后都免不了使用公共父类AbstractApplicationContext提供的refresh()进行配置文件加载。

3、wac.refresh

// 来源于类 AbstractApplicationContext,跟父Context是同一个方法
// 熟悉的代码,但是注意细节上的逻辑还是跟父WebApplicationContext有些区别的
// 大致走一下
@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // Prepare this context for refreshing.
        // 跟之前逻辑是一样的
        prepareRefresh();

        // Tell the subclass to refresh the internal bean factory.
        // 这里的区别在于getConfigLocations返回的是/WEB-INF/dispatcher-servlet.xml
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);

        try {
            // Allows post-processing of the bean factory in context subclasses.
            // 以下代码逻辑跟之前都是一样的
            postProcessBeanFactory(beanFactory);

            // Invoke factory processors registered as beans in the context.
            invokeBeanFactoryPostProcessors(beanFactory);

            // Register bean processors that intercept bean creation.
            registerBeanPostProcessors(beanFactory);

            // Initialize message source for this context.
            initMessageSource();

            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();

            // Initialize other special beans in specific context subclasses.
            onRefresh();

            // Check for listener beans and register them.
            registerListeners();

            // Instantiate all remaining (non-lazy-init) singletons.
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            // 区别在于这一步
            finishRefresh();
        }

        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                        "cancelling refresh attempt: " + ex);
            }

            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();

            // Reset 'active' flag.
            cancelRefresh(ex);

            // Propagate exception to caller.
            throw ex;
        }

        finally {
            // Reset common introspection caches in Spring's core, since we
            // might not ever need metadata for singleton beans anymore...
            resetCommonCaches();
        }
    }
}

4、finishRefresh

protected void finishRefresh() {
    // Clear context-level resource caches (such as ASM metadata from scanning).
    clearResourceCaches();

    // Initialize lifecycle processor for this context.
    // new DefaultLifecycleProcessor
    initLifecycleProcessor();

    // Propagate refresh to lifecycle processor first.
    // new DefaultLifecycleProcessor().onRefresh()
    getLifecycleProcessor().onRefresh();

    // Publish the final event.
    // 关键在于这一步,发布最后一个事件,这个刷新事件会触发DispatchServlet中的onFresh方法
    publishEvent(new ContextRefreshedEvent(this));

    // Participate in LiveBeansView MBean, if active.
    LiveBeansView.registerApplicationContext(this);
}

5、onRefresh调用的时机

// 我们还是回到最开始的initWebApplicationContext方法,来自于类 FrameworkServlet
// 这里想强调是 onFresh 方法不是在这里被调用的
protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -> use it
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // No context instance is defined for this servlet -> create a local one
        wac = createWebApplicationContext(rootContext);
    }

    // 不是在这里被调用的
    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        synchronized (this.onRefreshMonitor) {
            onRefresh(wac);
        }
    }

    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}


// 而是在这里,来自于类FrameworkServlet的内部类 ContextRefreshListener
// 注入是在这个方法 configureAndRefreshWebApplicationContext 中的这行代码

// 注意这一行,后面会来回来的
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        FrameworkServlet.this.onApplicationEvent(event);
    }
}

// 类DispatcherServlet中进行了实现
@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

六、onFresh

在rootContext刷新完毕且servletContext刷新完后,servletContext还需要执行一次特别的刷新,这次刷新是为了完成对web的扩展。

// 类DispatcherServlet中进行了实现
@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

1、getDefaultStrategy

对于Spring MVC而言,有些组件是不可或缺,有些组件是可以没有的,当用户或系统没有指定特定组件时,Spring使用默认组件(可能Spring已经帮你指定了,但你还不知道,所以这些默认的并不是一定的,而要以实物为准)。

虽然是以实物为准,但大体上过一下还是很有必要的。以下是获取组件默认配置的方法

// 来自类 DispatcherServlet,initStrategies各子方法都会调用本方法以获取默认web组件
@SuppressWarnings("unchecked")
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
    String key = strategyInterface.getName();
    // key为类的全名,形如org.xxx.Xxxx
    String value = defaultStrategies.getProperty(key);
    if (value != null) {
        String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
        List<T> strategies = new ArrayList<>(classNames.length);
        for (String className : classNames) {
            try {
                Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
                Object strategy = createDefaultStrategy(context, clazz);
                strategies.add((T) strategy);
            }
            catch (ClassNotFoundException ex) {
                throw new BeanInitializationException(
                        "Could not find DispatcherServlet's default strategy class [" + className +
                        "] for interface [" + key + "]", ex);
            }
            catch (LinkageError err) {
                throw new BeanInitializationException(
                        "Unresolvable class definition for DispatcherServlet's default strategy class [" +
                        className + "] for interface [" + key + "]", err);
            }
        }
        return strategies;
    }
    else {
        return new LinkedList<>();
    }
}

具体的默认组件就是之前提到过的DispatcherServlet.properties中的内容(DispatcherServlet静态代码块),如下

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
	org.springframework.web.servlet.function.support.RouterFunctionMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
	org.springframework.web.servlet.function.support.HandlerFunctionAdapter


org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

继续追踪。

2、初始化MultipartResolver

在Spring中,MultipartResolver主要用来处理文件上传。在高版本的Spring提供了StandardServletMultipartResolver(基于Servlet3.0)和CommonsMultipartResolver(需要引入apache-common-upload架包)。

MultipartResolver就是在initMultipartResolver(context)中被加入到DispatcherServlet中的。

private void initMultipartResolver(ApplicationContext context) {
    try {
        this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
        if (logger.isDebugEnabled()) {
            logger.debug("Using MultipartResolver [" + this.multipartResolver + "]");
        }
    }
    catch (NoSuchBeanDefinitionException ex) {
        // Default is no multipart resolver.
        this.multipartResolver = null;
        if (logger.isDebugEnabled()) {
            logger.debug("Unable to locate MultipartResolver with name '" + MULTIPART_RESOLVER_BEAN_NAME +
                    "': no multipart request handling provided");
        }
    }
}

因为之前的步骤已经完成了Spring中配置文件的解析,所以在这里只要是在配置文件注册过的都可以通过ApplicationContext提供的getBean方法来直接获取对应的bean,进而初始化MultipartResolver。

这个组件Spring默认是不配置的,如果我们想支持文件上传,需要自己显示声明。

3、初始化LocaleResolver

在Spring的国际化配置中一共有3种使用方式。分别如下

  1. 基于URL参数的配置

通过URL参数来控制国际化,比如你在页面上加一句<a href=“?locale=zn_CH”>简体中文</a>来控制项目中使用的国际化参数。而提供这个功能的就是AcceptHeaderLocaleResolver,默认的参数名为locale,注意大小写。里面放的就是你的提交参数,比如en_US、zh_CN之类的,具体配置如下

<bean id="localeResolver" class="org.springframework.web.servlet.il8n.AcceptHeaderLocaleResolver"/>
  1. 基于session的配置

他通过检验用户会话中预置的属性来解析区域。最常用的是根据用户本次会话过程中的语言设定语言种类(例如,用户登录时选择语言种类,则此次登录周期内同一使用此语言设定),如果该会话属性不存在,它会根据accept-language Http头部确定默认区域。

<bean id="localeResolver" class="org.springframework.web.servlet.il8n.SessionLocaleResolver"/>
  1. 基于Cookie的国际化配置

CookieLocaleResolver用于通过浏览器的cookie设置取得Locale对象。这种策略在应用程序不支持会话或者状态必须保存在客户端时有用,配置如下

<bean id="localeResolver" class="org.springframework.web.servlet.il8n.CookieLocaleResolver"/>

这3种方式都可以解决国际化的问题,但是,对于LocaleResolver的使用基础是在DispatcherServlet中的初始化。

private void initLocaleResolver(ApplicationContext context) {
    try {
        this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
        if (logger.isDebugEnabled()) {
            logger.debug("Using LocaleResolver [" + this.localeResolver + "]");
        }
    }
    catch (NoSuchBeanDefinitionException ex) {
        // We need to use the default.
        this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
        if (logger.isDebugEnabled()) {
            logger.debug("Unable to locate LocaleResolver with name '" + LOCALE_RESOLVER_BEAN_NAME +
                    "': using default [" + this.localeResolver + "]");
        }
    }
}

4、初始化ThemeResolver

在Web开发中经常会遇到通过主题Theme来控制网页风格,这将进一步改善用户体验。简单地说,一个主题就是一组静态资源(比如样式表和图片),他们可以影响应用程序的视觉效果。Spring中的主题功能和国际化功能非常相似。构成Spring主题主要包括如下内容。

  • 主题资源

org.springframework.ui.context.ThemeSource是Spring中主题资源的接口,Spring的主题需要通过ThemeSource接口来实现存放主题信息的资源。

org.springframework.ui.context.support.ResourceBundleThemeSource是ThemeSource接口默认实现类(也就是通过ResourceBundle资源的方式定义主题),在Spring中配置如下

<bean id="themeSource" class="org.springframework.ui.context.support.ResourceBundle.ThemeSource>
	<property name="basenamePrefix" value="com.text. "></property>
</bean>	

默认状态下是在类路径根目录下查找相应的资源文件,也可以通过basenamePrefix来指定。这样,DispatcherServlet就会在com.test包下查找资源文件。

  • 主题解析器

ThemeSource定义了一些主题资源,那么不同的用户使用什么主题资源由谁定义呢?org.springframework.web.servlet.ThemeResolver是主题解析器的接口,主题解析的工作便是由它的子类来完成。对于主题解析器的子类主要有3个比较常用的实现,以主题文件summmer.properties为例

1)FixedThemeResolver用于选择一个固定的主题。

<bean id="themeResolver" class="org.springframework.web.servlet.theme.FixedThemeResolver">
	<property name="defaultThemeName" value="summer"/>
</bean>

以上配置的作用是设置主题文件为summer.properties,在整个项目内固定不变。

2)CookieThemeResolver用于实现用户所选的主题,以cookie的形式存放在客户端的机器上,配置如下

<bean id="themeResolver" class="org.springframework.web.servlet.theme.CookieThemeResolver">
	<property name="defaultThemeName" value="summer"/>
</bean>

3)SessionThemeResolver用于把主题保存在用户的Http的session中

<bean id="themeResolver" class="org.springframework.web.servlet.theme.SsessionThemeResolver">
	<property name="defaultThemeName" value="summer"/>
</bean>

4)AbstractThemeResolver是一个抽象类被SessionThemeResolver和FixedThemeResolver继承,用户也可以继承它来自定义主题解析器。

  • 拦截器

如果需要根据用户请求来改变主题,那么Spring提供了一个已经实现的拦截器ThemeChangeInterceptor拦截器了,配置如下

<bean id="themeChangeInterceptor" class="org.springframework.web.servlet.theme.ThemeChangeInterceptor">
	<property name="paramName" value="themeName"/>
</bean>

其中设置用户请求名参数为themeName,即URL为themeName=具体的主题名称。此外还需要在handlerMapping中配置拦截器,需要在HandleMapping中添加拦截器。

<property name="interceptors">
	<list>
		<ref local="themeChangeInterceptor">
	</list>
</property>

了解了主题文件的简单使用方式后,再来查看解析器的初始化工作,与其他变量的初始化工作相同,主题文件解析器的初始化工作并没有任何需要说明的地方。

private void initThemeResolver(ApplicationContext context) {
    try {
        this.themeResolver = context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class);
        if (logger.isDebugEnabled()) {
            logger.debug("Using ThemeResolver [" + this.themeResolver + "]");
        }
    }
    catch (NoSuchBeanDefinitionException ex) {
        // We need to use the default.
        this.themeResolver = getDefaultStrategy(context, ThemeResolver.class);
        if (logger.isDebugEnabled()) {
            logger.debug(
                    "Unable to locate ThemeResolver with name '" + THEME_RESOLVER_BEAN_NAME + "': using default [" +
                            this.themeResolver + "]");
        }
    }
}

5、初始化HandlerMappings

当客户端发出request时,DispatcherServlet会将request提交给HandleMapping,然后HandlerMapping根据WebApplicationContext的配置来回传给DispatcherServlet中相应的Controller。

在基于Spring MVC的Web应用程序中,我们可以为DispatcherServlet提供多个HandlerMapping供其使用。DispatcherServlet在选用HandlerMapping的过程中,将根据我们所指定的一系列HandlerMapping的优先级进行排序,然后优先使用优先级在前的HandlerMapping。

如果当前HandlerMapping能够返回可用的Handler,DispatcherServlet则使用当前返回的Handler进行Web请求的处理,而不在继续询问其他的HandlerMapping。否则,DispatchServlet将继续按照各个HandlerMapping的优先级进行询问,直到获取一个可用的Handler为止。初始化配置如下

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
	// 默认为true
    if (this.detectAllHandlerMappings) {
        // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.
            OrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        try {
            HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default HandlerMapping later.
        }
    }

    // Ensure we have at least one HandlerMapping, by registering
    // a default HandlerMapping if no other mappings are found.
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
        if (logger.isDebugEnabled()) {
            logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
        }
    }
}

默认情况下,Spring MVC将加载当前系统中所有实现HandlerMapping接口的bean。如果只期望Spring MVC加载指定的HandlerMapping时,可以修改web.xml中的DispatcherServlet的初始参数,将detectAllHandlerMappings的值设置为false。

<init-param>
	<param-name>detectAllHandleMappings</param-name>
	<param-value>false</param-value>
</init-param>

此时,Spring MVC将查找名为“handleMapping”的bean,并作为当前系统唯一的HandlerMapping。如果没有定义HandlerMapping的话,则Spring MVC将按照org.springframework.web.servlet.DispatcherServlet所在目录下的DispatcherServlet.properties中所定义的org.springframework.web.servlet.HandlerMapping的内容来加载默认的HandlerMapping(用户没有自定义Strategies的情况下)。

6、初始化HandlerAdapters

从名字也能联想到这是一个典型的适配器模式的使用,在计算机编程中,适配器模式将一个类的接口适配成用户所期待的。使用适配器,可以使接口不兼容而无法在一起工作的类协同工作,做法是将类自己的接口包裹在一个已存在的类中。那么在处理Handler时为什么会使用适配器模式呢?回答这个问题我们首先要分析它的初始化逻辑。

private void initHandlerAdapters(ApplicationContext context) {
    this.handlerAdapters = null;

    if (this.detectAllHandlerAdapters) {
        // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerAdapter> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerAdapters = new ArrayList<HandlerAdapter>(matchingBeans.values());
            // We keep HandlerAdapters in sorted order.
            OrderComparator.sort(this.handlerAdapters);
        }
    }
    else {
        try {
            HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
            this.handlerAdapters = Collections.singletonList(ha);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default HandlerAdapter later.
        }
    }

    // Ensure we have at least some HandlerAdapters, by registering
    // default HandlerAdapters if no other adapters are found.
    if (this.handlerAdapters == null) {
        // 默认策略
        this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
        if (logger.isDebugEnabled()) {
            logger.debug("No HandlerAdapters found in servlet '" + getServletName() + "': using default");
        }
    }
}

同样在初始化的过程中涉及了一个变量detectAllHandlerAdapters,detectAllHandlerAdapters作用和detectAllHandlerMappings类似。只不过在无法找到对应的bean(handlerAdapter),系统会尝试加载默认的适配器。

在getDefaultStrategies方法中,Spring会尝试从defaultStrategies中加载对应的HandlerAdapter的属性。如果程序开发人员没有在配置文件中定义自己的适配器,那么Spring会默认加载配置文件中的3个适配器。

作为总控制器的DispatcherServlet通过处理器映射得到处理器后,会轮询处理器适配器模块,查找能够处理当前Http请求的处理器适配器的实现,处理器适配器模块根据处理器映射返回的处理器类型,例如简单的控制器类型、注解控制器类型或远程调用处理器类型,来选择某一个适当的处理器适配器的实现,从而适配当前的Http请求。

1、Http请求处理器适配器(HttpRequestHandlerAdapter)

Http请求处理器适配器仅仅支持对Http请求处理器的适配。它简单地将Http请求对象和响应对象传递给Http请求处理器的实现,它不需要返回值。主要应用在基于Http的远程调用的实现上。

2、简单控制器处理器适配器(SimpleControllerHandlerAdapter)

这个实现类将Http请求适配到一个控制器的实现进行处理。这里的控制器的实现是一个简单的控制器接口的实现。简单控制器处理器适配器被设计成一个框架类的实现,不需要被改写,客户化的业务逻辑通常是在控制器接口的实现类中实现的。

3、注解方法处理器适配器(AnnotationMethodHandlerAdapter)

这个类的实现是基于注解的实现,它需要结合注解方法映射和注解方法处理器协同工作。它通过解析声明在注解控制器的请求映射信息来解析相应的处理器方法来处理当前Http请求。在处理的过程中,它通过反射来发现探测处理器方法的参数,调用处理器方法,并且映射返回值到模型和控制器对象,最后返回模型和控制器对象给作为主控制器的DispatcherServlet。

我们现在基本上可以回答之前的问题了,Spring中所使用的Handler并没有任何特殊的联系,但是为了统一处理,Spring提供了不同情况下的适配器。

7、初始化HandlerExceptionResolvers

基于HandlerExceptionResolver接口的异常处理,使用这种方式只需要实现resolveException方法,该方法返回一个ModelAndView对象,在方法内部对异常的类型进行判断,然后尝试生成对应的ModelAndView对象,如果该方法返回了null,则Spring会继续寻找其他的实现了HandlerExceptionResolver接口的bena。换句话说,Spring会搜索所有注册在其环境中的实现了HandlerExceptionResolver接口的bean,逐个执行,直到返回一个ModelAndView对象。

public interface HandlerExceptionResolver {

	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);

}
private void initHandlerExceptionResolvers(ApplicationContext context) {
    this.handlerExceptionResolvers = null;

    if (this.detectAllHandlerExceptionResolvers) {
        // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
                .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values());
            // We keep HandlerExceptionResolvers in sorted order.
            OrderComparator.sort(this.handlerExceptionResolvers);
        }
    }
    else {
        try {
            HandlerExceptionResolver her =
                    context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
            this.handlerExceptionResolvers = Collections.singletonList(her);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, no HandlerExceptionResolver is fine too.
        }
    }

    // Ensure we have at least some HandlerExceptionResolvers, by registering
    // default HandlerExceptionResolvers if no other resolvers are found.
    if (this.handlerExceptionResolvers == null) {
        this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
        if (logger.isDebugEnabled()) {
            logger.debug("No HandlerExceptionResolvers found in servlet '" + getServletName() + "': using default");
        }
    }
}

8、初始化RequestToViewNameTranslator

当Controller处理器方法没有返回一个View对象或逻辑视图名称,并且在该方法中没有直接往response的输出流里面写数据的时候,Spring就会采用约定好的方式提供一个逻辑实录名称。这个逻辑视图名称是通过Spring定义的org.springframework.web.servlet.RequestToViewNameTranslator接口的getViewName方法来实现的,我们可以实现自己的RequestToViewNameTranslator接口来约定好没有返回视图名称的时候如何确定视图名称。

Spring已经给我们提供了一个它自己的实现,那就是org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator。

在介绍DefaultRequestToViewNameTranslator是如何约定视图名称之前,我们先来看一下它支持用户定义的属性:

  • prefix:前缀,表示约定好的视图名称需要加上的前缀,默认空串。
  • suffix:后缀,表示约定好的视图名称需要加上的后缀,默认空串。
  • separator:分隔符,默认是斜杠“/”。
  • stripLeadinSlash:如果首字符是分隔符,是否去除,默认true。
  • stripTrailingSlash:如果最后一个字符是分隔符,是否去除,默认true。
  • stripExtension:如果请求路径包含扩展名是否要去除,默认true。
  • urlDecode:是否对URL解码,默认true。它会采用请求中指定的编码对URL进行解码。

这里举一个例子进行说明,假设请求路径为https://www.xiaokui.site/blog/good-looking-hk/test.html,真是请求URL为/blog/good-looking-hk/test.html,则有如下结论

  • prefix和suffix如果都存在,其他为默认值,那么对应的逻辑视图名称应该是{prefix}blog/good-looking-hk/test{suffix}。
  • stripLeadinSlash和stripTrailingSlash如果都为false,其他默认,这时候对应的逻辑视图应该是/blog/good-looking-hk/test.html
  • 如果是采用默认配置的话,返回的逻辑视图名称应该是blog/good-looking-hk/test。
private void initRequestToViewNameTranslator(ApplicationContext context) {
    try {
        this.viewNameTranslator =
                context.getBean(REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class);
        if (logger.isTraceEnabled()) {
            logger.trace("Detected " + this.viewNameTranslator.getClass().getSimpleName());
        }
        else if (logger.isDebugEnabled()) {
            logger.debug("Detected " + this.viewNameTranslator);
        }
    }
    catch (NoSuchBeanDefinitionException ex) {
        // We need to use the default.
        this.viewNameTranslator = getDefaultStrategy(context, RequestToViewNameTranslator.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No RequestToViewNameTranslator '" + REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME +
                    "': using default [" + this.viewNameTranslator.getClass().getSimpleName() + "]");
        }
    }
}

9、初始化ViewResolvers

在Spring MVC中,当Controller将请求处理结果放入ModelAndView中以后,DispacherServlet会根据ModelAndView选择合适的视图进行渲染。ViewResolver接口定义了resolveViewName方法,根据viewName创建合适类型的View实现。

如下是一个经典的jsp配置

// 提供默认InternalResourceViewResolver实现,但默认前后缀均为空串
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/"/>
    <property name="suffix" value=".jsp"/>
</bean>

viewResolver相关属性的初始化工作在initViewResolvers中完成的。

private void initViewResolvers(ApplicationContext context) {
    this.viewResolvers = null;
    if (this.detectAllViewResolvers) {
        // Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
        Map<String, ViewResolver> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.viewResolvers = new ArrayList<>(matchingBeans.values());
            // We keep ViewResolvers in sorted order.
            AnnotationAwareOrderComparator.sort(this.viewResolvers);
        }
    }
    else {
        try {
            ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
            this.viewResolvers = Collections.singletonList(vr);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default ViewResolver later.
        }
    }

    // Ensure we have at least one ViewResolver, by registering
    // a default ViewResolver if no other resolvers are found.
    if (this.viewResolvers == null) {
        this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No ViewResolvers declared for servlet '" + getServletName() +
                    "': using default strategies from DispatcherServlet.properties");
        }
    }
}

10、初始化FlashMapManager

Spring MVC Flash attributes提供了一个请求存储属性,可供其他请求使用。在使用重定向时候非常必要,例如Post、Redirect、Get模式。Flash attributes在重定向之前暂存(就像存在session中)以便重定向之后还能使用,并立即删除。

Spring MVC有两个主要的抽象来支持flash attributes。FlashMap用于保持flash attributes,而FlashMapManager用于存储、检索、管理FlashMap实例。

flash attribute支持默认开启,并不需要显示启用,它永远不会导致Http Seesion的创建。这两个FlashMap实例都可以通过静态方法RequestContextUtils从Spring MVC的任何位置访问。初始化代码如下

private void initFlashMapManager(ApplicationContext context) {
    try {
        this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
        if (logger.isTraceEnabled()) {
            logger.trace("Detected " + this.flashMapManager.getClass().getSimpleName());
        }
        else if (logger.isDebugEnabled()) {
            logger.debug("Detected " + this.flashMapManager);
        }
    }
    catch (NoSuchBeanDefinitionException ex) {
        // We need to use the default.
        this.flashMapManager = getDefaultStrategy(context, FlashMapManager.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No FlashMapManager '" + FLASH_MAP_MANAGER_BEAN_NAME +
                    "': using default [" + this.flashMapManager.getClass().getSimpleName() + "]");
        }
    }
}

至此,DispatchServlet已经初始化完成,特别的,有如下输出日志

15:41:57.563 [0.1][INFO] web.servlet.FrameworkServlet 525: Initializing Servlet 'dispatcher'
# 中间省略若干行....
15:42:04.758 [0.1][INFO] web.servlet.FrameworkServlet 547: Completed initialization in 7195 ms

下篇我们将详细分析DispatchServlet中的doService、doGet、doPost等具体实现逻辑。

总访问次数: 382次, 一般般帅 创建于 2020-02-27, 最后更新于 2024-10-30

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!